LOADING...

加载过慢请开启缓存(浏览器默认开启)

loading

c++(基础部分)

2021/4/26

本文负责记录作者偷袭!cpp中的知识要点

主要还是以自己舒服的方式记的这些笔记,一些特别基础的点就跳过了

呜呜呜发现自己确实应该连c语言的内存部分都还不是很明白,再开一坑吧

1.从C到C++

命名空间

为了解决合作开发时的命名冲突问题,c++引入了命名空间(Namespace)的概念。请看下面的例子:

namespace Li
{
  //小李的变量定义
    FILE fp = NULL;
}
namespace Han
{  //小韩的变量定义
    FILE fp = NULL
}

小李与小韩各自定义了以自己姓氏为名的命名空间,此时再将他们的 fp 变量放在一起编译就不会有任何问题。

输入和输出

在编写 C++ 程序时,如果需要使用输入输出时,则需要包含头文件iostream,它包含了用于输入输出的对象,例如常见的cin表示标准输入、cout表示标准输出、cerr表示标准错误。

cout 和 cin 都是 C++ 的内置对象,而不是关键字。cout、cin 的用法非常强大灵活。

C++中的const又玩出了新花样

因为感觉这篇挺需要实例讲解的所以直接整篇直接copy走了

C++中的 const 更像编译阶段的 #define

先来看下面的两条语句:

const int m = 10;int n = m;

我们知道,变量是要占用内存的,即使被 const 修饰也不例外。m、n 两个变量占用不同的内存,int n = m;表示将 m 的值赋给 n,这个赋值的过程在C和C++中是有区别的。

在C语言中,编译器会先到 m 所在的内存取出一份数据,再将这份数据赋给 n;而在C++中,编译器会直接将 10 赋给 n,没有读取内存的过程,和int n = 10;的效果一样。C++ 中的常量更类似于#define命令,是一个值替换的过程,只不过#define是在预处理阶段替换,而常量是在编译阶段替换。

C++ 对 const 的处理少了读取内存的过程,优点是提高了程序执行效率,缺点是不能反映内存的变化,一旦 const 变量被修改,C++ 就不能取得最新的值。

有读者提出疑问,const 变量不是禁止被修改吗?对,这种说法没错!不过这只是语法层面上的限制,通过指针仍然可以修改。下面的代码演示了如何通过指针修改 const 变量:

#include <stdio.h>int main(){    const int n = 10;    int *p = (int*)&n;  //必须强制类型转换    *p = 99;  //修改const变量的值    printf("%d\n", n);    return 0;}

注意,&n得到的指针的类型是const int *,必须强制转换为int *后才能赋给 p,否则类型是不兼容的。

将代码放到.c文件中,以C语言的方式编译,运行结果为99。再将代码放到.cpp文件中,以C++的方式编译,运行结果就变成了10。这种差异正是由于C和C++对 const 的处理方式不同造成的。

在C语言中,使用 printf 输出 n 时会到内存中获取 n 的值,这个时候 n 所在内存中的数据已经被修改成了 99,所以输出结果也是 99。而在C++中,printf("%d\n", n);语句在编译时就将 n 的值替换成了 10,效果和printf("%d\n", 10);一样,不管 n 所在的内存如何变化,都不会影响输出结果。

当然,这种修改常量的变态代码在实际开发中基本不会出现,本例只是为了说明C和C++对 const 的处理方式的差异:C语言对 const 的处理和普通变量一样,会到内存中读取数据;C++ 对 const 的处理更像是编译时期的#define,是一个值替换的过程。

C++中全局 const 变量的可见范围是当前文件

我们知道,普通全局变量的作用域是当前文件,但是在其他文件中也是可见的,使用extern声明后就可以使用,这在《C语言头文件的编写》一章中进行了深入讲解。下面是多文件编程的演示代码:

代码段1(源文件1):

#include <stdio.h>int n = 10;void func();int main(){    func();    printf("main: %d\n", n);    return 0;}

代码段2(源文件2):

#include <stdio.h>extern int n;void func(){    printf("module: %d\n", n);}

不管是以C还是C++的方式编译,运行结果都是:
module: 10
main: 10

在C语言中,const 变量和普通变量一样,在其他源文件中也是可见的。修改代码段1,在 n 的定义前面加 const 限制,如下所示:

const int n = 10;

修改后的代码仍然能够正确编译,运行结果和上面也是一样的。这说明C语言中的 const 变量在多文件编程时的表现和普通变量一样,除了不能修改,没有其他区别。

但是如果按照C++的方式编译(将源文件后缀设置为.cpp),修改后的代码就是错误的。这是因为 C++ 对 const 的特性做了调整,C++ 规定,全局 const 变量的作用域仍然是当前文件,但是它在其他文件中是不可见的,这和添加了static关键字的效果类似。虽然代码段2中使用 extern 声明了变量 n,但是在链接时却找不到代码段1中的 n。

由于 C++ 中全局 const 变量的可见范围仅限于当前源文件,所以可以将它放在头文件中,这样即使头文件被包含多次也不会出错,请看下面的例子。

module.h 代码:

const int n = 10;void func();

module.cpp 代码:

#include <stdio.h>#include "module.h"void func(){    printf("module: %d\n", n);}

main.cpp 代码:

#include <stdio.h>#include "module.h"int main(){    func();    printf("main: %d\n", n);    return 0;}

运行结果:
module: 10
main: 10

C和C++中全局 const 变量的作用域相同,都是当前文件,不同的是它们的可见范围:C语言中 const 全局变量的可见范围是整个程序,在其他文件中使用 extern 声明后就可以使用;而C++中 const 全局变量的可见范围仅限于当前文件,在其他文件中不可见,所以它可以定义在头文件中,多次引入后也不会出错。

C++ new和delete运算符

在C语言中,动态分配内存用 malloc() 函数,释放内存用 free() 函数。如下所示:

int *p = (int*) malloc( sizeof(int) * 10 );  //分配10个int型的内存空间free(p);  //释放内存

C++中,这两个函数仍然可以使用,但是C++又新增了两个关键字,new 和 delete

new 用来动态分配内存,delete 用来释放内存。用 new 和 delete 分配内存更加简单:

int *p = new int;  //分配1个int型的内存空间delete p;  //释放内存

new 操作符会根据后面的数据类型来推断所需空间的大小。

如果希望分配一组连续的数据,可以使用 new[]:

int *p = new int[10];  //分配10个int型的内存空间delete[] p;

和 malloc() 一样,new 也是在堆区分配内存,必须手动释放,否则只能等到程序运行结束由操作系统回收。用 new[] 分配的内存需要用 delete[] 释放,它们是一一对应的。

c++内联函数

指定内联函数的方法很简单,只需要在函数定义处增加 inline 关键字

1. //内联函数,交换两个数的值
2. **inline** void swap(int *a, int *b){
3.   int temp;
4.   temp = *a;
5.   *a = *b;
6.   *b = temp;
7. }

当函数比较复杂时,函数调用的时空开销可以忽略,大部分的 CPU 时间都会花费在执行函数体代码上,所以我们一般是将非常短小的函数声明为内联函数。编译后的程序会存在多份相同的函数拷贝,如果被声明为内联函数的函数体非常大,那么编译后的程序体积也将会变得很大,所以再次强调,一般只将那些短小的、频繁调用的函数声明为内联函数。

在函数声明处添加 inline 关键字是无效的。内联函数不应该有声明,应该将函数定义放在本应该出现函数声明的地方,这是一种良好的编程风格。

函数重载

函数的重载的规则:

  • 函数名称必须相同。
  • 参数列表必须不同(个数不同、类型不同、参数排列顺序不同等)。
  • 函数的返回类型可以相同也可以不相同。
  • 仅仅返回类型不同不足以成为函数的重载。

2.类与对象

定义和基础语法

基础语法:

一个简单的类的定义:

纯文本复制
class Student{
public:
//成员变量 
char *name;
int age;
float score;
//成员函数
void say(){
cout<<name<<"的年龄是"<<age<<",成绩是"<<score<<endl;   
}
};

注意在类定义的最后有一个分号;

创建对象:

class Student LiLei;  //正确
Student LiLei;  //同样正确
Student allStu[100];//创建对象数组

使用对象指针

Student stu;
Student *pStu = &stu;

上面代码中创建的对象 stu 在栈上分配内存,需要使用&获取它的地址,例如:

pStu 是一个指针,它指向 Student 类型的数据,也就是通过 Student 创建出来的对象。

在栈上创建出来的对象都有一个名字,比如 stu,使用指针指向它不是必须的。

Student *pStu = new Student;

当然也可以在堆上创建对象,这个时候就需要使用前面讲到的new关键字

使用 new 在堆上创建出来的对象是匿名的,它在堆上分配内存,没有名字,只能得到一个指向它的指针,所以必须使用一个指针变量来接收这个指针,再借助指针来访问它的成员变量或成员函数。

在实际开发中,new 和 delete 往往成对出现,记得 delete 掉不再使用的对象依然是一种良好的编程习惯。

类的成员变量和成员函数详解

成员函数是一个类的成员,出现在类体中,它的作用范围由类来决定;

普通函数是独立的,作用范围是全局的,或位于某个命名空间内。

当成员函数定义在类外时,就必须在函数名前面加上类名予以限定。::被称为域解析符(也称作用域运算符或作用域限定符),用来连接类名和函数名,指明当前函数属于哪个类。

类成员的访问权限以及类的封装

Java、c# 程序员注意,C++ 中的 public、private、protected 只能修饰类的成员,不能修饰类,C++中的类没有共有私有之分。

public:全都行!

protected:只能访问

private:只能在类内部使用,在类外都是无效的。

封装:

实际项目开发中的成员变量以及只被成员函数调用的成员函数都建议声明为 private

给成员变量赋值的函数通常称为 set 函数,它们的名字通常以set开头,后跟成员变量的名字;

读取成员变量的值的函数通常称为 get 函数,它们的名字通常以get开头,后跟成员变量的名字。

对象的内存模型

类是创建对象的模板,不占用内存空间,不存在于编译后的可执行文件中;而对象是实实在在的数据,需要内存来存储。

编译器会将成员变量和成员函数分开存储:分别为每个对象的成员变量分配内存,但是所有对象都共享同一段函数代码。

C++函数编译原理和成员函数的实现

C++和C语言的编译方式不同。C语言中的函数在编译时名字不变,或者只是简单的加一个下划线_例如,func() 编译后为 func() 或 _func()。

只要函数所在的命名空间、所属的类、包含的参数列表等有一个不同,最后产生的新函数名也不同。

C++规定,编译成员函数时要额外添加一个参数,把当前对象的指针传递进去,通过指针来访问成员变量。

假设 Demo 类有两个 int 型的成员变量,分别是 a 和 b,并且在成员函数 display() 中使用到了,如下所示:

void Demo::display()
{    
cout<<a<<endl;
cout<<b<<endl;
}

那么编译后的代码类似于:

void new_function_name(Demo * const p)
{    //通过指针p来访问a、b
cout<<p->a<<endl;
cout<<p->b<<endl;
}

使用obj.display()调用函数时,也会被编译成类似下面的形式:

new_function_name(&obj);

这样通过传递对象指针就完成了成员函数和成员变量的关联。这与我们从表明上看到的刚好相反,通过对象调用成员函数时,不是通过对象找函数,而是通过函数找对象。

构造函数详解

构造函数没有返回值,因为没有变量来接收返回值,即使有也毫无用处,这意味着:

  • 不管是声明还是定义,函数名前面都不能出现返回值类型,即使是 void 也不允许;
  • 函数体中不能有 return 语句。

构造函数的调用是强制性的,一旦在类中定义了构造函数,那么创建对象时就一定要调用,不调用是错误的。

如果有多个重载的构造函数,那么创建对象时提供的实参必须和其中的一个构造函数匹配;

调用没有参数的构造函数也可以省略括号。

在栈上创建对象可以写作Student stu()Student stu

在堆上创建对象可以写作Student *pstu = new Student()Student *pstu = new Student,它们都会调用构造函数 Student()。

初始化列表:

在函数首部与函数体之间添加了一个冒号:,后面紧跟m_name(name), m_age(age), m_score(score)语句

这个语句的意思相当于函数体内部的m_name = name; m_age = age; m_score = score;语句,也是赋值的意思。

使用构造函数初始化列表并没有效率上的优势,仅仅是书写方便,尤其是成员变量较多时,这种写法非常简单明了。

构造函数初始化列表还有一个很重要的作用,那就是初始化 const 成员变量。初始化 const 成员变量的唯一方法就是使用初始化列表

析构函数详解

析构函数(Destructor):一种特殊的成员函数,没有返回值,不需要程序员显式调用(程序员也没法显式调用),而是在销毁对象时自动执行。

构造函数的名字和类名相同,而析构函数的名字是在类名前面加一个~符号。

注意:析构函数没有参数,不能被重载,因此一个类只能有一个析构函数。如果用户没有定义,编译器会自动生成一个默认的析构函数。

执行时机:

在所有函数之外创建的对象是全局对象,它和全局变量类似,位于内存分区中的全局数据区,程序在结束执行时会调用这些对象的析构函数。

new 创建的对象位于堆区,通过 delete 删除时才会调用析构函数;如果没有 delete,析构函数就不会被执行。

成员对象和封闭类详解

封闭类对象生成时,先执行所有成员对象的构造函数,然后才执行封闭类自己的构造函数。

封闭类对象消亡时,先执行封闭类的析构函数,然后再执行成员对象的析构函数

成员对象析构函数的执行次序和构造函数的执行次序相反,即先构造的后析构,这是 C++ 处理此类次序问题的一般规律。

this指针详解(精辟中的精辟)

ps:因为原文说标注是精辟所以我觉得自己必然是精辟中的精辟

this 是c++中的一个关键字,也是一个 const指针,它指向当前对象,通过它可以访问当前对象的所有成员。

this 只能用在类的内部,通过 this 可以访问类的所有成员,包括 private、protected、public 属性的。

本例中成员函数的参数和成员变量重名,只能通过 this 区分。

以成员函数setname(char *name)为例,它的形参是name,和成员变量name重名,如果写作name = name;就是给形参name赋值,而不是给成员变量name赋值。

而写作this -> name = name;后,=左边的name就是成员变量,右边的name就是形参,一目了然。

this 作为隐式形参,本质上是成员函数的局部变量,所以只能用在成员函数的内部,并且只有在通过对象调用成员函数时才给 this 赋值。

static详解

static成员变量详解:

在c++中,我们可以使用静态成员变量来实现多个对象共享数据的目标。静态成员变量是一种特殊的成员变量,它被关键字static修饰

我们可以使用静态成员变量来实现多个对象共享数据的目标。静态成员变量是一种特殊的成员变量,它被关键字static修饰,例如:

class Student{
public:
    Student(char *name, int age, float score);
    void show();
public:
    static int m_total;
    //静态成员变量
private:
    char *m_name;
    int m_age;
    float m_score;
};

这段代码声明了一个静态成员变量 m_total,用来统计学生的人数。

static 成员变量属于类,不属于某个具体的对象,即使创建多个对象,也只为 m_total 分配一份内存,所有对象使用的都是这份内存中的数据。当某个对象修改了 m_total,也会影响到其他对象。

static 成员变量必须在类声明的外部初始化,具体形式为:

type class::name = value;

type 是变量的类型,class 是类名,name 是变量名,value 是初始值。将上面的 m_total 初始化:

int Student::m_total = 0;

静态成员变量在初始化时不能再加 static,但必须要有数据类型。被 private、protected、public 修饰的静态成员变量都可以用这种方式初始化。

注意:static 成员变量的内存既不是在声明类时分配,也不是在创建对象时分配,而是在(类外)初始化时分配。反过来说,没有在类外初始化的 static 成员变量不能使用。

static 成员变量不占用对象的内存,而是在所有对象之外开辟内存,即使不创建对象也可以访问。static 成员变量和普通的 static 变量类似,都在内存分区中的全局数据区分配内存。

  1. 一个类中可以有一个或多个静态成员变量,所有的对象都共享这些静态成员变量,都可以引用它。
  2. 普通成员变量在对象创建时分配内存,在对象销毁时释放内存。
  3. static 成员变量和普通 static 变量一样,都在内存分区中的全局数据区分配内存,到程序结束时才释放。这就意味着,static 成员变量不随对象的创建而分配内存,也不随对象的销毁而释放内存。
  4. 静态成员变量必须初始化,而且只能在类体外进行。
  5. 静态成员变量既可以通过对象名访问,也可以通过类名访问,但要遵循 private、protected 和 public 关键字的访问权限限制。当通过对象名访问时,对于不同的对象,访问的是同一份内存。

static静态成员函数详解:

static还能声明静态成员函数

根本区别:普通成员函数可以访问所有成员(包括成员变量和成员函数),但静态成员函数没有 this 指针,不知道指向哪个对象,无法访问对象的成员变量,所以只能访问静态成员变量。

const

const成员函数

const 成员变量的用法和普通 const 变量的用法相似,需要在声明和定义的时候在函数头部的结尾加上 const 关键字

初始化 const 成员变量只有一种方法,就是通过构造函数的初始化列表

const 成员函数可以使用类中的所有成员变量,但是不能修改它们的值,这种措施主要还是为了保护数据而设置的。

必须在成员函数的声明和定义处同时加上 const 关键字。

最后再来区分一下 const 的位置:

  • 函数开头的 const 用来修饰函数的返回值,表示返回值是 const 类型,也就是不能被修改,例如const char * getname()
  • 函数头部的结尾加上 const 表示常成员函数,这种函数只能读取成员变量的值,而不能修改成员变量的值,例如char * getname() const

const对象:

const 也可以用来修饰对象,称为常对象。

一旦将对象定义为常对象之后,就只能调用类的 const 成员(包括 const 成员变量和 const 成员函数)了,因为非 const 成员可能会修改对象的数据(编译器也会这样假设),C++禁止这样做。。

friend

友元函数:

在函数声明前面加 friend 关键字就构成了友元函数。

可以将全局函数声明为友元函数。也可以声明其他类的成员函数为友元,此时可以访问当前类中的所有成员,包括 public、protected、private 属性的。

友元函数不同于类的成员函数,在友元函数中不能直接访问类的成员,必须要借助对象。

友元类:

将类 B 声明为类 A 的友元类,那么类 B 中的所有成员函数都是类 A 的友元函数,可以访问类 A 的所有成员,包括 public、protected、private 属性的。

关于友元:

  • 友元的关系是单向的而不是双向的。如果声明了类 B 是类 A 的友元类,不等于类 A 是类 B 的友元类,类 A 中的成员函数不能访问类 B 中的 private 成员。
  • 友元的关系不能传递。如果类 B 是类 A 的友元类,类 C 是类 B 的友元类,不等于类 C 是类 A 的友元类。

string详解

string 是 C++ 中常用的一个类,使用 string 类需要包含头文件<string>

与C风格的字符串不同,当我们需要知道字符串长度时,可以调用 string 类提供的 length() 函数。

有了 string 类,我们可以使用++=运算符来直接拼接字符串

+来拼接字符串时:

运算符的两边可以都是 string 字符串

可以是一个 string 字符串和一个C风格的字符串

可以是一个 string 字符串和一个字符数组

可以是一个 string 字符串和一个单独的字符。

增删改查:

insert() 函数可以在 string 字符串中指定的位置插入另一个字符串,它的一种原型为:

string& insert (size_t pos, const string& str);

erase() 函数可以删除 string 中的一个子字符串。它的一种原型为:

string& erase (size_t pos = 0, size_t len = npos);

substr() 函数用于从 string 字符串中提取子字符串,它的原型为:

string substr (size_t pos = 0, size_t len = npos) const;

find() 函数用于在 string 字符串中查找子字符串出现的位置,它其中的两种原型为:

size_t find (const string& str, size_t pos = 0) const;
size_t find (const char* s, size_t pos = 0) const;

rfind() 和 find() 很类似,同样是在字符串中查找子字符串,不同的是 find() 函数从第二个参数开始往后查找,而 rfind() 函数则最多查找到第二个参数处

find_first_of() 函数用于查找子字符串和字符串共同具有的字符在字符串中首次出现的位置。

总结

类的成员有成员变量和成员函数两种。

成员函数之间可以互相调用,成员函数内部可以访问成员变量。

私有成员只能在类的成员函数内部访问。默认情况下,class 类的成员是私有的,struct 类的成员是公有的。

可以用“对象名.成员名”、“引用名.成员名”、“对象指针->成员名”的方法访问对象的成员变量或调用成员函数。成员函数被调用时,可以用上述三种方法指定函数是作用在哪个对象上的。

对象所占用的存储空间的大小等于各成员变量所占用的存储空间的大小之和(如果不考虑成员变量对齐问题的话)。

定义类时,如果一个构造函数都不写,则编译器自动生成默认(无参)构造函数和复制构造函数。如果编写了构造函数,则编译器不自动生成默认构造函数。一个类不一定会有默认构造函数,但一定会有复制构造函数。

任何生成对象的语句都要说明对象是用哪个构造函数初始化的。即便定义对象数组,也要对数组中的每个元素如何初始化进行说明。如果不说明,则编译器认为对象是用默认构造函数或参数全部可以省略的构造函数初始化。在这种情况下,如果类没有默认构造函数或参数全部可以省略的构造函数,则编译出错。

对象在消亡时会调用析构函数。

每个对象有各自的一份普通成员变量,但是静态成员变量只有一份,被所有对象所共享。静态成员函数不具体作用于某个对象。即便对象不存在,也可以访问类的静态成员。静态成员函数内部不能访问非静态成员变量,也不能调用非静态成员函数。

常量对象上面不能执行非常量成员函数,只能执行常量成员函数。

包含成员对象的类叫封闭类。任何能够生成封闭类对象的语句,都要说明对象中包含的成员对象是如何初始化的。如果不说明,则编译器认为成员对象是用默认构造函数或参数全部可以省略的构造函数初始化。

在封闭类的构造函数的初始化列表中可以说明成员对象如何初始化。封闭类对象生成时,先执行成员对象的构造函数,再执行自身的构造函数;封闭类对象消亡时,先执行自身的析构函数,再执行成员对象的析构函数。

const 成员和引用成员必须在构造函数的初始化列表中初始化,此后值不可修改。

友元分为友元函数和友元类。友元关系不能传递。

成员函数中出现的 this 指针,就是指向成员函数所作用的对象的指针。因此,静态成员函数内部不能出现 this 指针。成员函数实际上的参数个数比表面上看到的多一个,多出来的参数就是 this 指针。


本章完成于4.16 0:27


3.引用

定义和基础语法

参数传递的本质=>赋值,赋值=>对内存进行拷贝

内存拷贝=>将一块内存上的数据复制到另一块内存上。

问题抛出:数组、结构体、对象是一系列数据的集合,数据的数量没有限制,对它们进行频繁的内存拷贝可能会消耗很多时间

解决方案:传递指针或引用(Reference)(c++专属)

定义:引用可以看做是数据的一个别名,通过这个别名和原来的名字都能够找到这份数据。

定义方式:类似于指针,只是用&取代了*

type &name = data;

规范:必须在定义的同时初始化,并且以后也要从一而终,不能再引用其它数据,这有点类似于常量(const 变量)。引用在**定义时需要添加&,在使用时不能添加&**,使用时添加&表示取地址。

变量 r 就是变量 a 的引用,它们用来指代同一份数据;也可以说变量 r 是变量 a 的另一个名字。由于引用 r 和原始变量 a 都是指向同一地址,所以通过引用也可以修改原始变量中所存储的数据,请看下面的例子:

#include <iostream>
using namespace std;
int main() {
int a = 99;    
int &r = a;    
r = 47;    
cout << a << ", " << r << endl;
return 0;
}

运行结果:
47, 47


如果不希望通过引用来修改原始的数据,那么可以在定义时添加 const 限制

const type &name = value;

也可以是:

type const &name = value;

这种引用方式为常引用

但绝不是这一种!!!

int & const r = a;

引用作为函数参数

将函数的形参指定为引用的形式,在调用函数时就会将实参和形参绑定在一起,让它们都指代同一份数据。因此如果在函数体中修改了形参的数据,那么实参的数据也会被修改,

//直接传递参数内容
void swap1(int a, int b) {
int temp = a;
a = b;
b = temp;
}
//传递指针
void swap2(int *p1, int *p2) {
int temp = *p1;
 *p1 = *p2;
*p2 = temp;
}
//按引用传参
void swap3(int &r1, int &r2) {
int temp = r1;
r1 = r2;
r2 = temp;

Input two integers: 12 34↙
12 34
Input two integers: 88 99↙
99 88
Input two integers: 100 200↙
200 100

按引用传参在使用形式上比指针更加直观,鼓励大量使用引用,一般可以代替指针。

引用作为函数返回值

引用除了可以作为函数形参,还可以作为函数返回值,请看下面的例子:

int &plus10(int &r) 
{
r += 10;    
return r;
}
int main()
{    
int num1 = 10; 
int num2 = plus10(num1);  
cout << num1 << " " << num2 << endl;  
return 0;
}

运行结果:
20 20

不能返回局部数据(例如局部变量、局部对象、局部数组等)的引用,因为当函数调用完成后局部数据就会被销毁

引用在本质上和指针有什么区别?

看下面的例子:

int num = 99;

class A
{
public:
A();
private:
int n; 
int &r;
};

A::A(): n(0), r(num){}
int main ()
{    
A *a = new A(); 
cout<<sizeof(A)<<endl;  //输出A类型的大小 
cout<<hex<<showbase<<*((int*)a + 1)<<endl;  //输出r本身的内容    cout<<&num<<endl;  //输出num变量的地址  
return 0;
}

运行结果:
8
0x442000
0x442000

从运行结果可以看出:

  • 成员变量 r 是占用内存的,如果不占用的话,sizeof(A)的结果应该为 4。
  • r 存储的内容是0x442000,也即变量 num 的地址。

r 的实现和指针非常类似。如果将 r 定义为int *类型的指针,并在构造函数中让它指向 num,那么 r 占用的内存也是 4 个字节,存储的内容也是 num 的地址。

其实引用只是对指针进行了简单的封装,它的底层依然是通过指针实现的,引用占用的内存和指针占用的内存长度一样,在 32 位环境下是 4 个字节,在 64 位环境下是 8 个字节

其他区别:

  1. 引用必须在定义时初始化,并且以后也要从一而终,不能再指向其他数据;而指针没有这个限制,指针在定义时不必赋值,以后也能指向任意数据。
  2. 指针可以有多级,但是引用只能有一级,例如,int **p是合法的,而int &&r是不合法的。
  3. 指针和引用的自增(++)自减(–)运算意义不一样。对指针使用 ++ 表示指向下一份数据,对引用使用 ++ 表示它所指代的数据本身加 1;

4.继承与派生

定义和基础语法

继承(Inheritance)可以理解为一个类从另一个类获取成员变量和成员函数的过程。例如类 B 继承于类 A,那么 B 就拥有 A 的成员变量和成员函数。

派生(Derive)和继承是一个概念,只是站的角度不同。继承是儿子接收父亲的产业,派生是父亲把产业传承给儿子。

被继承的类称为父类或基类,继承的类称为子类或派生类。“子类”和“父类”通常放在一起称呼,“基类”和“派生类”通常放在一起称呼。

定义方式:

下面我们定义一个基类 People,然后由此派生出 Student 类:

//基类 People
class People{

public: 
void setname(char *name);  
void setage(int age);  
char *getname();  
int getage();

private:   
char *m_name;  
int m_age;
};

void People::setname(char *name)
{ m_name = name; }

void People::setage(int age)
{ m_age = age; }

char* People::getname()
{ return m_name; }

int People::getage()
{ return m_age;}

//派生类 Student
class Student: public People{

public:   
void setscore(float score); 
float getscore();

private: 
float m_score;
};

void Student::setscore(float score)
{ m_score = score; }

float Student::getscore()
{ return m_score; }

int main(){ 
Student stu; 
stu.setname("小明");  
stu.setage(16);  
stu.setscore(95.5f);  
cout<<stu.getname()<<"的年龄是 "<<stu.getage()<<",成绩是 "<<stu.getscore()<<endl; 
return 0;
}

运行结果:
小明的年龄是 16,成绩是 95.5

Student 类继承了 People 类的成员,同时还新增了自己的成员变量 score 和成员函数 setscore()、getscore()。这些继承过来的成员,可以通过子类对象访问,就像自己的一样。

继承中的三种权限修饰符

继承的一般语法为:

class 派生类名:[权限修饰符] 基类名{
派生类新增加的成员
};

权限修饰符限定了基类成员在派生类中的访问权限,包括 public(公有的)private(私有的)protected(受保护的)

此项是可选项,如果不写,默认为 private(成员变量和成员函数默认也是 private)。

基类中的 protected 成员可以在派生类中使用,而基类中的 private 成员不能在派生类中使用。

public、protected、private

不同的继承方式会影响基类成员在派生类中的访问权限。

public继承方式

  • 基类中所有 public 成员在派生类中为 public 属性;
  • 基类中所有 protected 成员在派生类中为 protected 属性;
  • 基类中所有 private 成员在派生类中不能使用。

protected继承方式

  • 基类中的所有 public 成员在派生类中为 protected 属性;
  • 基类中的所有 protected 成员在派生类中为 protected 属性;
  • 基类中的所有 private 成员在派生类中不能使用。

private继承方式

  • 基类中的所有 public 成员在派生类中均为 private 属性;
  • 基类中的所有 protected 成员在派生类中均为 private 属性;
  • 基类中的所有 private 成员在派生类中不能使用。

由于 private 和 protected 继承方式会改变基类成员在派生类中的访问权限,导致继承关系复杂,所以实际开发中我们一般使用 public。

继承时的名字遮蔽

如果派生类中的成员(包括成员变量和成员函数)和基类中的成员重名,那么就会遮蔽从基类继承过来的成员。在派生类中使用该成员时,实际上使用的是派生类新增的成员,而不是从基类继承来的。

基类中的成员仍然可以访问,不过要加上类名和域解析符

 Student stu("小明", 16, 90.5);
 //使用的是派生类新增的成员函数,而不是从基类继承的
    stu.show();
 //使用的是从基类继承来的成员函数
    stu.People::show();

基类成员函数和派生类成员函数不构成重载

对于成员函数而言,不管函数的参数如何,只要名字一样就会造成遮蔽。

基类成员函数和派生类成员函数不会构成重载,如果派生类有同名函数,那么就会遮蔽基类中的所有同名函数,不管它们的参数是否一样。

类继承时的作用域嵌套

每个类都会定义它自己的作用域,当存在继承关系时,派生类的作用域嵌套在基类的作用域之内,如果一个名字在派生类的作用域内无法找到,编译器会继续到外层的基类作用域中查找该名字的定义。

恰恰因为类作用域有这种继承嵌套的关系,所以派生类才能像使用自己的成员一样来使用基类的成员。

只有一个作用域内的同名函数才具有重载关系,不同作用域内的同名函数是会造成遮蔽,使得外层函数无效。派生类和基类拥有不同的作用域,所以它们的同名函数不具有重载关系。

继承时的对象内存模型

没有继承时对象的内存模型很简单,成员变量和成员函数会分开存储:

  • 对象的内存中只包含成员变量,存储在栈区或堆区(使用 new 创建对象);
  • 成员函数与对象内存分离,存储在代码区。

存在继承关系时,内存模型会稍微复杂一些。


有继承关系时,派生类的内存模型可以看成是基类成员变量和新增成员变量的总和

而所有成员函数仍然存储在另外一个区域——代码区,由所有对象共享。

image-20210420195935279

有成员变量遮蔽时的内存分布

image-20210420200802092

当基类 A、B 的成员变量被遮蔽时,仍然会留在派生类对象 obj_c 的内存中,C 类新增的成员变量始终排在基类 A、B 的后面。

基类和派生类的构造函数

基类的成员函数可以被继承,但这仅仅指的是普通的成员函数,类的构造函数不能被继承。

设计派生类时,对继承过来的成员变量的初始化也要由派生类的构造函数完成。但大部分基类成员变量都有 private 属性,在派生类中无法访问,更不能使用派生类的构造函数来初始化。

解决这个问题的思路是:在派生类的构造函数中调用基类的构造函数。

Student::Student(char *name, int age, float score):
People(name, age), m_score(score){ }

People(name, age)就是调用基类的构造函数,并将 name 和 age 作为实参传递给它,m_score(score)是派生类的参数初始化表,它们之间以逗号,隔开。也可以将基类构造函数的调用放在参数初始化表后面。

但是不管顺序如何,派生类构造函数总是先调用基类构造函数再执行其他代码。

定义派生类构造函数时最好指明基类构造函数;如果不指明,就调用基类的默认构造函数(不带参数的构造函数);如果没有默认构造函数,那么编译失败。

派生类构造函数中只能调用直接基类的构造函数,不能调用间接基类的。

以 A–>B–>C 为例,C 是最终的派生类,B 就是 C 的直接基类,A 就是 C 的间接基类。

构造函数的调用顺序

创建派生类对象时,会先调用基类构造函数,再调用派生类构造函数。

构造函数的调用顺序是按照继承的层次自顶向下、从基类再到派生类的。

基类和派生类的构造函数

析构函数也不能被继承。与构造函数不同的是,在派生类的析构函数中不用显式地调用基类的析构函数。

另外析构函数的执行顺序和构造函数的执行顺序也刚好相反:

  • 创建派生类对象时,构造函数的执行顺序和继承顺序相同,即先执行基类构造函数,再执行派生类构造函数。
  • 而销毁派生类对象时,析构函数的执行顺序和继承顺序相反,即先执行派生类析构函数,再执行基类析构函数。

多继承

即一个派生类可以有两个或多个基类称为多继承

多继承的语法如下:

class D: public A, private B, protected C{
  //类D新增加的成员
}

D 是多继承形式的派生类,它以公有的方式继承 A 类,以私有的方式继承 B 类,以保护的方式继承 C 类。

多继承下的构造函数

D 类构造函数的写法为:

D(形参列表): A(实参列表), B(实参列表), C(实参列表){
  //其他操作
}

基类构造函数的调用顺序和和它们在派生类构造函数中出现的顺序无关,而是和声明派生类时基类出现的顺序相同。

命名冲突

当两个或多个基类中(这两个类彼此不是继承关系,不然就覆盖了)有同名的成员时,如果直接访问该成员,就会产生命名冲突

这个时候需要在成员名字前面加上类名和域解析符::,以显式地指明到底使用哪个类的成员,消除二义性。

虚继承和虚基类详解

虚继承

问题抛出:菱形继承

image-20210420205828007

类 A 中的成员变量和成员函数继承到类 D 中变成了两份

一份来自 A–>B–>D 这条路径

另一份来自 A–>C–>D 这条路径。

解决方案:为了解决多继承时的命名冲突和冗余数据问题,c++提出了虚继承,使得在派生类中只保留一份间接基类的成员

继承方式前面加上 virtual 关键字就是虚继承

//间接基类A
class A{
protected:
    int m_a;
};
//直接基类B
class B: virtual public A{  //虚继承
protected:
    int m_b;
};
//直接基类C
class C: virtual public A{  //虚继承
protected:
    int m_c;
};
class D: public B, public C{
public:
    void seta(int a){ m_a = a; }  //正确
    void setb(int b){ m_b = b; }  //正确
    void setc(int c){ m_c = c; }  //正确
    void setd(int d){ m_d = d; }  //正确

这个被共享的基类就称为虚基类(Virtual Base Class)

在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。

虚派生只影响从指定了虚基类的派生类中进一步派生出来的类,它不会影响派生类本身。

C++标准库中的 iostream 类就是一个虚继承的实际应用案例。

iostream 从 istream 和 ostream 直接继承而来,而 istream 和 ostream 又都继承自一个共同的名为 base_ios 的类,是典型的菱形继承。此时 istream 和 ostream 必须采用虚继承,否则将导致 iostream 类中保留两份 base_ios 类的成员。

虚继承时的构造函数

最终派生类的构造函数必须要调用虚基类的构造函数。对最终的派生类来说,虚基类是间接基类,而不是直接基类。这跟普通继承不同。

在普通继承中,派生类构造函数中只能调用直接基类的构造函数,不能调用间接基类的。

D::D(int a, int b, int c, int d): A(a), B(90, b), C(100, c), m_d(d){ }

在最终派生类 D 的构造函数中,除了调用 B 和 C 的构造函数,还调用了 A 的构造函数,这说明 D 不但要负责初始化直接基类 B 和 C,还要负责初始化间接基类 A。

而在以往的普通继承中,派生类的构造函数只负责初始化它的直接基类,再由直接基类的构造函数初始化间接基类,用户尝试调用间接基类的构造函数将导致错误。

派生类赋值给基类(向上转型)

派生类赋值给基类,包括将派生类对象赋值给基类对象、将派生类指针赋值给基类指针将派生类引用赋值给基类引用,这在 C++ 中称为向上转型(Upcasting)

相应地,将基类赋值给派生类称为向下转型(Downcasting)。

向上转型非常安全,可以由编译器自动完成;向下转型有风险,需要程序员手动干预。

下面的例子演示了如何将派生类对象赋值给基类对象:

//基类
class A{

public:  
A(int a);  
void display();

public:  
int m_a;
};

A::A(int a): m_a(a){ }
void A::display(){    cout<<"Class A: m_a="<<m_a<<endl;}

//派生类
class B: public A{

public: 
B(int a, int b); 
void display();

public:  
int m_b;
};

B::B(int a, int b): A(a), m_b(b){ }
void B::display()
{    cout<<"Class B: m_a="<<m_a<<", m_b="<<m_b<<endl;}

int main(){
A a(10);  
B b(66, 99); 
//赋值前 
a.display(); 
b.display(); 
cout<<"--------------"<<endl; 
//赋值后   
a = b;   
a.display();  
b.display();  
return 0;}

运行结果:
Class A: m_a=10
Class B: m_a=66, m_b=99
-—————————
Class A: m_a=66
Class B: m_a=66, m_b=99

赋值的本质是将现有的数据写入已分配好的内存中,对象的内存只包含了成员变量,所以对象之间的赋值是成员变量的赋值,成员函数不存在赋值问题。


指针和引用的向上转型爷不写辣!!!4.20 21:42毕


5.多态与虚函数

定义和基本语法

问题抛出:

void People::display(){
    cout<<m_name<<"今年"<<m_age<<"岁了,是个无业游民。"<<endl;}
void Teacher::display(){
    cout<<m_name<<"今年"<<m_age<<"岁了,是一名教师,每月有"<<m_salary<<"元的收入。"<<endl;}

int main(){
    People *p = new People("王志刚", 23);
    p -> display();
    p = new Teacher("赵宏佳", 45, 8200);
    p -> display();

运行结果:
王志刚今年23岁了,是个无业游民。
赵宏佳今年45岁了,是个无业游民。

直观上认为,如果指针指向了派生类对象,那么就应该使用派生类的成员变量和成员函数,这符合人们的思维习惯。但是本例的运行结果却告诉我们,当基类指针 p 指向派生类 Teacher 的对象时,虽然使用了 Teacher 的成员变量,但是却没有使用它的成员函数,

通过基类指针只能访问派生类的成员变量,但是不能访问派生类的成员函数。

解决方案:c++增加了虚函数(Virtual Function)只需要在函数声明前面增加 virtual 关键字

public: 
 virtual void display();  //声明为虚函数

运行结果:
王志刚今年23岁了,是个无业游民。
赵宏佳今年45岁了,是一名教师,每月有8200元的收入。


有了虚函数,基类指针可以按照基类的方式来做事,也可以按照派生类的方式来做事,它有多种表现方式,我们将这种现象称为多态(Polymorphism)

提供多态的目的是:可以通过基类指针对所有派生类(包括直接派生和间接派生)的成员变量和成员函数进行“全方位”的访问,尤其是成员函数。如果没有多态,我们只能访问成员变量。

so

通过指针调用普通的成员函数时会根据指针的类型(通过哪个类定义的指针)来判断调用哪个类的成员函数

虚函数是根据指针的指向来调用的,指针指向哪个类的对象就调用哪个类的虚函数。


多态是面向对象编程的主要特征之一,C++中虚函数的唯一用处就是构成多态。


虚函数注意事项以及构成多态的条件

  1. 只需要在虚函数的声明处加上 virtual 关键字,函数定义处可以加也可以不加。
  2. 可以只将基类中的函数声明为虚函数,这样所有派生类中具有遮蔽关系的同名函数都将自动成为虚函数。
  3. 基类中定义了虚函数时,如果派生类没有定义新的函数来遮蔽此函数,那么将使用基类的虚函数。
  4. 只有派生类的虚函数覆盖基类的虚函数(函数原型相同)才能构成多态(通过基类指针访问派生类函数)。例如基类虚函数的原型为virtual void func();,派生类虚函数的原型为virtual void func(int);,那么当基类指针 p 指向派生类对象时,语句p -> func(100);将会出错,而语句p -> func();将调用基类的函数。
  5. 派生类不继承基类的构造函数,所以构造函数也不能是虚函数。
  6. 析构函数可以声明为虚函数,而且有时候必须要声明为虚函数

构成多态的条件:

  • 必须存在继承关系;
  • 继承关系中必须有同名的虚函数,并且它们是覆盖关系(函数原型相同)。
  • 存在基类的指针,通过该指针调用虚函数。

虚析构函数的必要性

//基类
class Base{
public:
    Base();
    ~Base();
protected:
    char *str;
};
Base::Base(){
    str = new char[100];
    cout<<"Base constructor"<<endl;
}
Base::~Base(){
    delete[] str;
    cout<<"Base destructor"<<endl;
}
//派生类
class Derived: public Base{
public:
    Derived();
    ~Derived();
private:
    char *name;
};
Derived::Derived(){
    name = new char[100];
    cout<<"Derived constructor"<<endl;
}
Derived::~Derived(){
    delete[] name;
    cout<<"Derived destructor"<<endl;
}
int main(){
   Base *pb = new Derived();
   delete pb;
   cout<<"-------------------"<<endl;
   Derived *pd = new Derived();
   delete pd;
   return 0;
}

运行结果:
Base constructor
Derived constructor
Base destructor
-——————
Base constructor
Derived constructor
Derived destructor
Base destructor

pb、pd 分别是基类指针和派生类指针,它们都指向派生类对象,最后使用 delete 销毁 pb、pd 所指向的对象

从运行结果可以看出,语句delete pb;只调用了基类的析构函数,没有调用派生类的析构函数;而语句delete pd;同时调用了派生类和基类的析构函数。

为什么delete pb;不会调用派生类的析构函数呢?

因为这里的析构函数是非虚函数。pb 是基类的指针,所以不管它指向基类的对象还是派生类的对象,始终都是调用基类的析构函数。通过指针访问非虚函数时,编译器会根据指针的类型来确定要调用的函数。

基类的析构函数声明为虚函后,派生类的析构函数也会自动成为虚函数。编译器会忽略指针的类型,而根据指针的指向来选择函数。

pb、pd 都指向了派生类的对象,所以会调用派生类的析构函数,继而再调用基类的析构函数。如此一来也就解决了内存泄露的问题。

纯虚函数和抽象类详解

可以将虚函数声明为纯虚函数:

virtual 返回值类型 函数名 (函数参数) = 0;

纯虚函数没有函数体,只有函数声明,包含纯虚函数的类称为抽象类(Abstract Class)

之所以说它抽象,是因为它无法实例化,也就是无法创建对象。纯虚函数没有函数体,不是完整的函数,无法调用,也无法为其分配内存空间。

抽象类通常是作为基类,让派生类去实现纯虚函数。派生类必须实现纯虚函数才能被实例化。

在实际开发中,你可以定义一个抽象基类,只完成部分功能,未完成的功能交给派生类去实现(谁派生谁实现)。这部分未完成的功能,往往是基类不需要的,或者在基类中无法实现的。虽然抽象基类没有完成,但是却强制要求派生类完成。抽象基类用来约束派生类的功能。

注意事项:

一个纯虚函数就可以使类成为抽象基类,但是抽象基类中除了包含纯虚函数外,还可以包含其它的成员函数(虚函数或普通函数)和成员变量。

typeid运算符:获取类型信息()

typeid 运算符用来获取一个表达式的类型信息

  • 对于基本类型(int、float 等c++内置类型)的数据,类型信息所包含的内容比较简单,主要是指数据的类型。
  • 对于类类型的数据(也就是对象),类型信息是指对象所属的类、所包含的成员、所在的继承关系等。
typeid( dataType )
typeid( expression )

4.22 0:04完成多态和虚函数这部分啦,还有一些细节但我不打算深究了,有空再记录吧


6.运算符重载

当时在之前粗略的学cpp的那本书上,运算符重载完全看不懂,所以对这块内容还是蛮恐惧的,希望可以通过这次学习解惑!


定义和基本语法

感觉这一篇实例很重要,所以把原文的内容几乎全都搬进来了


我们前面说到函数重载(Function Overloading)可以让一个函数名有多种功能,在不同情况下进行不同的操作。

运算符重载(Operator Overloading)也是一个道理,同一个运算符可以有不同的功能。我们已经在不知不觉中使用了运算符重载。+号可以对不同类型(int、float 等)的数据进行加法操作;<<既是位移运算符,又可以配合 cout 向控制台输出数据。c++本身已经对这些运算符进行了重载。


基本语法如下:

#include <iostream>
using namespace std;
class complex{
public:
    complex();
    complex(double real, double imag);
public:
    //声明运算符重载
    complex operator+(const complex &A) const;
    void display() const;
private:
    double m_real;  //实部
    double m_imag;  //虚部
};
complex::complex(): m_real(0.0), m_imag(0.0){ }
complex::complex(double real, double imag): m_real(real), m_imag(imag){ }
//实现运算符重载
complex complex::operator+(const complex &A) const{
    complex B;
    B.m_real = this->m_real + A.m_real;
    B.m_imag = this->m_imag + A.m_imag;
    return B;
}
void complex::display() const{
    cout<<m_real<<" + "<<m_imag<<"i"<<endl;
}
int main(){
    complex c1(4.3, 5.8);
    complex c2(2.4, 3.7);
    complex c3;
    c3 = c1 + c2;
    c3.display();
    return 0;
}

运行结果:
6.7 + 9.5i

operator是关键字,专门用于定义重载运算符的函数。我们可以将operator 运算符名称这一部分看做函数名,对于上面的代码,函数名就是operator+

运算符重载函数除了函数名有特定的格式,其它地方和普通函数并没有区别。

运算符重载是通过函数实现的,它本质上是函数重载。格式如下:

返回值类型 operator 运算符名称 (形参表列){
    //TODO:
}

我们在 complex 类中重载了运算符+,该重载只对 complex 对象有效。当执行c3 = c1 + c2;语句时,编译器检测到+号左边+号具有左结合性,所以先检测左边)是一个 complex 对象,就会调用成员函数operator+(),也就是转换为下面的形式:

c3 = c1.operator+(c2);

上面的运算符重载还可以有更加简练的定义形式:

complex complex::operator+(const complex &A)const
{
return complex(this->m_real + A.m_real, this->m_imag + A.m_imag);
}

return 语句会创建一个临时对象,这个对象没有名称,是一个匿名对象。在创建临时对象过程中调用构造函数,return 语句将该临时对象作为函数返回值。


在全局范围内重载运算符

运算符重载函数不仅可以作为类的成员函数,还可以作为全局函数。更改上面的代码,在全局范围内重载+,实现复数的加法运算:

//声明为友元函数
    friend complex operator+(const complex &A, const complex &B);
//在全局范围内重载+
complex operator+(const complex &A, const complex &B){
    complex C;
    C.m_real = A.m_real + B.m_real;
    C.m_imag = A.m_imag + B.m_imag;
    return C;
}

运算符重载函数不是 complex 类的成员函数,但是却用到了 complex 类的 private 成员变量,所以必须在 complex 类中将该函数声明为友元函数。

当执行c3 = c1 + c2;语句时,编译器检测到+号两边都是 complex 对象,就会转换为类似下面的函数调用:

c3 = operator+(c1, c2);

运算符被重载后,原有的功能仍然保留,没有丧失或改变。通过运算符重载,扩大了C++已有运算符的功能,使之能用于对象。


运算符重载时遵循的规则

  1. 并不是所有的运算符都可以重载。能够重载的运算符包括:
    + - * / % ^ & | ~ ! = < > += -= = /= %= ^= &= |= << >> <<= >>= == != <= >= && || ++ – , -> -> () [] new new[] delete delete[]

    上述运算符中,[]是下标运算符,()是函数调用运算符。自增自减运算符的前置和后置形式都可以重载。长度运算符sizeof、条件运算符: ?、成员选择符.和域解析运算符::不能被重载。

  2. 重载不能改变运算符的优先级和结合性。假设上一节的 complex 类中重载了+号和*号,并且 c1、c2、c3、c4 都是 complex 类的对象,那么下面的语句:

    c4 = c1 + c2 * c3;
    

    等价于:

    c4 = c1 + ( c2 * c3 );
    

    乘法的优先级仍然高于加法,并且它们仍然是二元运算符。

  3. 重载不会改变运算符的用法,原本有几个操作数、操作数在左边还是在右边,这些都不会改变。例如~号右边只有一个操作数,+号总是出现在两个操作数之间,重载后也必须如此。

  4. 运算符重载函数不能有默认的参数,否则就改变了运算符操作数的个数,这显然是错误的。

  5. 箭头运算符->、下标运算符[ ]、函数调用运算符( )、赋值运算符=只能以成员函数的形式重载。


    我们知道,运算符重载函数既可以作为类的成员函数,也可以作为全局函数。

    将运算符重载函数作为类的成员函数时,二元运算符的参数只有一个,一元运算符不需要参数。之所以少一个参数,是因为这个参数是隐含的。

    上节的 complex 类中重载了加法运算符:

    complex operator+(const complex & A) const;
    

    当执行:

    c3 = c1 + c2;
    

    会被转换为:

    c3 = c1.operator+(c2);
    

    通过this指针隐式的访问 c1 的成员变量。


将运算符重载函数作为全局函数时,二元操作符就需要两个参数,一元操作符需要一个参数,而且其中必须有一个参数是对象,好让编译器区分这是程序员自定义的运算符,防止程序员修改用于内置类型的运算符的性质。

int operator + (int a,int b){
    return (a-b);
}

+号原来是对两个数相加,现在企图通过重载使它的作用改为两个数相减, 如果允许这样重载的话,那么表达式4+3的结果是 7 还是 1 呢?显然,这是绝对禁止的。

如果有两个参数,这两个参数可以都是对象,也可以一个是对象,一个是C ++内置类型的数据,例如:

complex operator+(int a, complex &c)
{    
return complex(a+c.real, c.imag);
}

它的作用是使一个整数和一个复数相加。

将运算符重载函数作为全局函数时,一般都需要在类中将该函数声明为友元函数。原因很简单,该函数大部分情况下都需要使用类的 private 成员。


重载数学运算符(实例演示)

四则运算符(+、-、、/、+=、-=、=、/=)和关系运算符(>、<、<=、>=、==、!=)都是数学运算符,它们在实际开发中非常常见,被重载的几率也很高,并且有着相似的重载格式。

本节以复数类 Complex 为例对它们进行重载,重在演示运算符重载的语法以及规范。复数能够进行完整的四则运算,但不能进行完整的关系运算:我们只能判断两个复数是否相等,但不能比较它们的大小,所以不能对 >、<、<=、>= 进行重载。

下面是具体的代码:

#include <iostream>
#include <cmath>
using namespace std;
//复数类
class Complex{
public:  //构造函数
    Complex(double real = 0.0, double imag = 0.0): m_real(real), m_imag(imag){ }
public:  //运算符重载
    //以全局函数的形式重载
    friend Complex operator+(const Complex &c1, const Complex &c2);
    friend Complex operator-(const Complex &c1, const Complex &c2);
    friend Complex operator*(const Complex &c1, const Complex &c2);
    friend Complex operator/(const Complex &c1, const Complex &c2);
    friend bool operator==(const Complex &c1, const Complex &c2);
    friend bool operator!=(const Complex &c1, const Complex &c2);
    //以成员函数的形式重载
    Complex & operator+=(const Complex &c);
    Complex & operator-=(const Complex &c);
    Complex & operator*=(const Complex &c);
    Complex & operator/=(const Complex &c);
public:  //成员函数
    double real() const{ return m_real; }
    double imag() const{ return m_imag; }
private:
    double m_real;  //实部
    double m_imag;  //虚部
};
//重载+运算符
Complex operator+(const Complex &c1, const Complex &c2){
    Complex c;
    c.m_real = c1.m_real + c2.m_real;
    c.m_imag = c1.m_imag + c2.m_imag;
    return c;
}
//重载-运算符
Complex operator-(const Complex &c1, const Complex &c2){
    Complex c;
    c.m_real = c1.m_real - c2.m_real;
    c.m_imag = c1.m_imag - c2.m_imag;
    return c;
}
//重载*运算符  (a+bi) * (c+di) = (ac-bd) + (bc+ad)i
Complex operator*(const Complex &c1, const Complex &c2){
    Complex c;
    c.m_real = c1.m_real * c2.m_real - c1.m_imag * c2.m_imag;
    c.m_imag = c1.m_imag * c2.m_real + c1.m_real * c2.m_imag;
    return c;
}
//重载/运算符  (a+bi) / (c+di) = [(ac+bd) / (c²+d²)] + [(bc-ad) / (c²+d²)]i
Complex operator/(const Complex &c1, const Complex &c2){
    Complex c;
    c.m_real = (c1.m_real*c2.m_real + c1.m_imag*c2.m_imag) / (pow(c2.m_real, 2) + pow(c2.m_imag, 2));
    c.m_imag = (c1.m_imag*c2.m_real - c1.m_real*c2.m_imag) / (pow(c2.m_real, 2) + pow(c2.m_imag, 2));
    return c;
}
//重载==运算符
bool operator==(const Complex &c1, const Complex &c2){
    if( c1.m_real == c2.m_real && c1.m_imag == c2.m_imag ){
        return true;
    }else{
        return false;
    }
}
//重载!=运算符
bool operator!=(const Complex &c1, const Complex &c2){
    if( c1.m_real != c2.m_real || c1.m_imag != c2.m_imag ){
        return true;
    }else{
        return false;
    }
}
//重载+=运算符
Complex & Complex::operator+=(const Complex &c){
    this->m_real += c.m_real;
    this->m_imag += c.m_imag;
    return *this;
}
//重载-=运算符
Complex & Complex::operator-=(const Complex &c){
    this->m_real -= c.m_real;
    this->m_imag -= c.m_imag;
    return *this;
}
//重载*=运算符
Complex & Complex::operator*=(const Complex &c){
    this->m_real = this->m_real * c.m_real - this->m_imag * c.m_imag;
    this->m_imag = this->m_imag * c.m_real + this->m_real * c.m_imag;
    return *this;
}
//重载/=运算符
Complex & Complex::operator/=(const Complex &c){
    this->m_real = (this->m_real*c.m_real + this->m_imag*c.m_imag) / (pow(c.m_real, 2) + pow(c.m_imag, 2));
    this->m_imag = (this->m_imag*c.m_real - this->m_real*c.m_imag) / (pow(c.m_real, 2) + pow(c.m_imag, 2));
    return *this;
}
int main(){
    Complex c1(25, 35);
    Complex c2(10, 20);
    Complex c3(1, 2);
    Complex c4(4, 9);
    Complex c5(34, 6);
    Complex c6(80, 90);
   
    Complex c7 = c1 + c2;
    Complex c8 = c1 - c2;
    Complex c9 = c1 * c2;
    Complex c10 = c1 / c2;
    cout<<"c7 = "<<c7.real()<<" + "<<c7.imag()<<"i"<<endl;
    cout<<"c8 = "<<c8.real()<<" + "<<c8.imag()<<"i"<<endl;
    cout<<"c9 = "<<c9.real()<<" + "<<c9.imag()<<"i"<<endl;
    cout<<"c10 = "<<c10.real()<<" + "<<c10.imag()<<"i"<<endl;
   
    c3 += c1;
    c4 -= c2;
    c5 *= c2;
    c6 /= c2;
    cout<<"c3 = "<<c3.real()<<" + "<<c3.imag()<<"i"<<endl;
    cout<<"c4 = "<<c4.real()<<" + "<<c4.imag()<<"i"<<endl;
    cout<<"c5 = "<<c5.real()<<" + "<<c5.imag()<<"i"<<endl;
    cout<<"c6 = "<<c6.real()<<" + "<<c6.imag()<<"i"<<endl;
   
    if(c1 == c2){
        cout<<"c1 == c2"<<endl;
    }
    if(c1 != c2){
        cout<<"c1 != c2"<<endl;
    }
   
    return 0;
}

运行结果:
c7 = 35 + 55i
c8 = 15 + 15i
c9 = -450 + 850i
c10 = 1.9 + -0.3i
c3 = 26 + 37i
c4 = -6 + -11i
c5 = 220 + 4460i
c6 = 5.2 + 1.592i
c1 != c2


推荐放到IDE里面去看,条理会清晰很多,感觉把这串理解透了,这一章节就差不多了


到底以成员函数还是全局函数(友元函数)的形式重载运算符

在上节的例子中,我们以全局函数的形式重载了 +、-、、/、==、!=,以成员函数的形式重载了 +=、-=、=、/=,没有一股脑都写成全局函数或者成员函数,为什么?


转换构造函数的概念

在分析以前,我们先来了解一个概念,叫做「转换构造函数」。先看例子

#include <iostream>
using namespace std;
//复数类
class Complex{
public:
    Complex(): m_real(0.0), m_imag(0.0){ }
    Complex(double real, double imag): m_real(real), m_imag(imag){ }
    //转换构造函数
    Complex(double real): m_real(real), m_imag(0.0){ }  
public:
    friend Complex operator+(const Complex &c1, const Complex &c2);
public:
    double real() const{ return m_real; }
    double imag() const{ return m_imag; }
private:
    double m_real;  //实部
    double m_imag;  //虚部
};
//重载+运算符
Complex operator+(const Complex &c1, const Complex &c2){
    Complex c;
    c.m_real = c1.m_real + c2.m_real;
    c.m_imag = c1.m_imag + c2.m_imag;
    return c;
}
int main(){
    Complex c1(25, 35);
    Complex c2 = c1 + 15.6;
    Complex c3 = 28.23 + c1;
    cout<<c2.real()<<" + "<<c2.imag()<<"i"<<endl;
    cout<<c3.real()<<" + "<<c3.imag()<<"i"<<endl;
   
    return 0;
}

运行结果:
40.6 + 35i
53.23 + 35i

留意第 30、31 行代码,它说明 Complex 类型可以和 double 类型相加,这很奇怪,因为我们并没有对针对这两个类型重载 +,这究竟是怎么做到的呢?

编译器在检测到 Complex 和 double(小数默认为 double 类型)相加时,会先尝试将 double 转换为 Complex,或者反过来将 Complex 转换为 double(只有类型相同的数据才能进行 + 运算),如果都转换失败,或者都转换成功(产生了二义性),才报错。本例中,编译器会先通过构造函数Complex(double real);将 double 转换为 Complex,再调用重载过的 + 进行计算。

Complex(double real);在作为普通构造函数的同时,还能将 double 类型转换为 Complex 类型,集合了“构造函数”和“类型转换”的功能,所以被称为「转换构造函数」。换句话说,转换构造函数用来将其它类型(可以是 bool、int、double 等基本类型,也可以是数组、指针、结构体、类等构造类型)转换为当前类类型。


为什么要以全局函数的形式重载 +

上面的例子中,我们定义的operator+是一个全局函数(一个友元函数),而不是成员函数,这样做是为了保证 + 运算符的操作数能够被对称的处理换句话说,小数(double 类型)在 + 左边和右边都是正确的。第 30 行代码中,15.6 在 + 的右边,第 31 行代码中,28.23 在 + 的左边,它们都能够被顺利地转换为 Complex 类型,所以不会出错。

如果将operator+定义为成员函数,根据“+ 运算符具有左结合性”这条原则,Complex c2 = c1 + 15.6;会被转换为下面的形式:

Complex c2 = c1.operator+(Complex(15.6));

这就是通过对象调用成员函数,是正确的。而对于Complex c3 = 28.23 + c1;,编译器会尝试转换为不同的形式:

Complex c3 = (28.23).operator+(c1);

很显然这是错误的,因为 double 类型并没有以成员函数的形式重载 +。

也就是说,以成员函数的形式重载 +,只能计算c1 + 15.6,不能计算28.23 + c1,这是不对称的

有读者可能会问,编译器明明可以把 28.23 先转换成 Complex 类型再相加呀,也就是下面的形式:

Complex c3 = Complex(28.23).operator+(c1);

为什么就是不转换呢?没错,编译器不会转换,原因在于,C++ 只会对成员函数的参数进行类型转换,而不会对调用成员函数的对象进行类型转换。以下面的语句为例:

obj.func(params);

编译器不会尝试对 obj 进行任何类型转换,它有 func() 成员函数就调用,没有就报错。而对于实参 params,编译器会“拼命地”将它转换为形参的类型。


为什么以成员函数形式重载+=

运算符重载的初衷是给类添加新的功能,方便类的运算,它作为类的成员函数是理所应当的,是首选的。不过,类的成员函数不能对称地处理数据,程序员必须在(参与运算的)所有类型的内部都重载当前的运算符。以上面的情况为例,我们必须在 Complex 和 double 内部都重载 + 运算符,这样做不但会增加运算符重载的数目,还要在许多地方修改代码,这显然不是我们所希望的,所以 C++ 进行了折中,允许以全局函数(友元函数)的形式重载运算符。

采用全局函数能使我们定义这样的运算符,它们的参数具有逻辑的对称性。与此相对应的,把运算符定义为成员函数能够保证在调用时对第一个(最左的)运算对象不出现类型转换,也就是上面提到的「C++ 不会对调用成员函数的对象进行类型转换」。

总的来说,有一部分运算符重载既可以是成员函数也可以是全局函数,我们应该优先考虑成员函数,这样更符合运算符重载的初衷;另外有一部分运算符重载必须是全局函数,这样能保证参数的对称性;除了 C++ 规定的几个特定的运算符外,暂时还没有发现必须以成员函数的形式重载的运算符。

C++ 规定,箭头运算符->、下标运算符[ ]、函数调用运算符( )、赋值运算符=只能以成员函数的形式重载。

重载<<与>>(输入与输出运算符)详解

标准库本身已经对左移运算符<<和右移运算符>>分别进行了重载,使其能够用于不同数据的输入输出,但是输入输出的对象只能是 C++ 内置的数据类型(例如 bool、int、double 等)和标准库所包含的类类型(例如 string、complex、ofstream、ifstream 等)。如果我们自己定义了一种新的数据类型,需要用输入输出运算符去处理,那么就必须对它们进行重载。

本节要达到的目标是让复数的输入输出和 int、float 等基本类型一样简单。假设 num1、num2 是复数,那么输出形式就是:

cout<<num1<<num2<<endl;

输入形式就是:

cin>>num1>>num2;

cout 是 ostream 类的对象,cin 是 istream 类的对象,要想达到这个目标,就必须以全局函数(友元函数)的形式重载<<>>,否则就要修改标准库中的类,这显然不是我们所期望的。


重载输入运算符>>

下面我们以全局函数的形式重载>>,使它能够读入两个 double 类型的数据,并分别赋值给复数的实部和虚部:

istream & operator>>(istream &in, complex &A)
{
in >> A.m_real >> A.m_imag;  
return in;
}

istream 表示输入流,cin 是 istream 类的对象,只不过这个对象是在标准库中定义的。之所以返回 istream 类对象的引用,是为了能够连续读取复数,让代码书写更加漂亮,例如:

complex c1, c2;
cin>>c1>>c2;

如果不返回引用,那就只能一个一个地读取了:

complex c1, c2;
cin>>c1;
cin>>c2;

另外,运算符重载函数中用到了 complex 类的 private 成员变量,必须在 complex 类中将该函数声明为友元函数:

friend istream & operator>>(istream & in , complex &a);

>>运算符可以按照下面的方式使用:

complex c;
cin>>c;

当输入1.45 2.34↙后,这两个小数就分别成为对象 c 的实部和虚部了。cin>> c;这一语句其实可以理解为:

operator>>(cin , c);

重载输出运算符<<

同样地,我们也可以模仿上面的形式对输出运算符>>进行重载,让它能够输出复数,请看下面的代码:

ostream & operator<<(ostream &out, complex &A)
{ 
out << A.m_real <<" + "<< A.m_imag <<" i ";
return out;
}

ostream 表示输出流,cout 是 ostream 类的对象。由于采用了引用的方式进行参数传递,并且也返回了对象的引用,所以重载后的运算符可以实现连续输出。

为了能够直接访问 complex 类的 private 成员变量,同样需要将该函数声明为 complex 类的友元函数:

friend ostream & operator<<(ostream &out, complex &A);

综合演示

#include <iostream>
using namespace std;
class complex{
public:
    complex(double real = 0.0, double imag = 0.0): m_real(real), m_imag(imag){ };
public:
    friend complex operator+(const complex & A, const complex & B);
    friend complex operator-(const complex & A, const complex & B);
    friend complex operator*(const complex & A, const complex & B);
    friend complex operator/(const complex & A, const complex & B);
    friend istream & operator>>(istream & in, complex & A);
    friend ostream & operator<<(ostream & out, complex & A);
private:
    double m_real;  //实部
    double m_imag;  //虚部
};
//重载加法运算符
complex operator+(const complex & A, const complex &B){
    complex C;
    C.m_real = A.m_real + B.m_real;
    C.m_imag = A.m_imag + B.m_imag;
    return C;
}
//重载减法运算符
complex operator-(const complex & A, const complex &B){
    complex C;
    C.m_real = A.m_real - B.m_real;
    C.m_imag = A.m_imag - B.m_imag;
    return C;
}
//重载乘法运算符
complex operator*(const complex & A, const complex &B){
    complex C;
    C.m_real = A.m_real * B.m_real - A.m_imag * B.m_imag;
    C.m_imag = A.m_imag * B.m_real + A.m_real * B.m_imag;
    return C;
}
//重载除法运算符
complex operator/(const complex & A, const complex & B){
    complex C;
    double square = A.m_real * A.m_real + A.m_imag * A.m_imag;
    C.m_real = (A.m_real * B.m_real + A.m_imag * B.m_imag)/square;
    C.m_imag = (A.m_imag * B.m_real - A.m_real * B.m_imag)/square;
    return C;
}
//重载输入运算符
istream & operator>>(istream & in, complex & A){
    in >> A.m_real >> A.m_imag;
    return in;
}
//重载输出运算符
ostream & operator<<(ostream & out, complex & A){
    out << A.m_real <<" + "<< A.m_imag <<" i ";;
    return out;
}
int main(){
    complex c1, c2, c3;
    cin>>c1>>c2;
    c3 = c1 + c2;
    cout<<"c1 + c2 = "<<c3<<endl;
    c3 = c1 - c2;
    cout<<"c1 - c2 = "<<c3<<endl;
    c3 = c1 * c2;
    cout<<"c1 * c2 = "<<c3<<endl;
    c3 = c1 / c2;
    cout<<"c1 / c2 = "<<c3<<endl;
    return 0;
}

运行结果:
2.4 3.6↙
4.8 1.7↙
c1 + c2 = 7.2 + 5.3 i
c1 - c2 = -2.4 + 1.9 i
c1 * c2 = 5.4 + 21.36 i
c1 / c2 = 0.942308 + 0.705128 i


今天到这里先结束吧 4.26 22:07


重载[](下标运算符)详解

c++规定,下标运算符[ ]必须以成员函数的形式进行重载。该重载函数在类中的声明格式如下:

返回值类型 & operator[ ] (参数);

或者:

const 返回值类型 & operator[ ] (参数) const;

使用第一种声明方式,[ ]不仅可以访问元素,还可以修改元素。使用第二种声明方式,[ ]只能访问而不能修改元素。

在实际开发中,我们应该同时提供以上两种形式,这样做是为了适应 const 对象,因为通过 const 对象只能调用 const 成员函数,如果不提供第二种形式,那么将无法访问 const 对象的任何元素。

下面我们通过自定义的 Array 类来实现变长数组(能使用变量指明数组长度)。

#include <iostream>
using namespace std;
class Array{
public:
    Array(int length = 0);
    ~Array();
public:
    int & operator[](int i);
    const int & operator[](int i) const;
public:
    int length() const { return m_length; }
    void display() const;
private:
    int m_length;  //数组长度
    int *m_p;  //指向数组内存的指针
};
Array::Array(int length): m_length(length){
    if(length == 0){
        m_p = NULL;
    }else{
        m_p = new int[length];
    }
}
Array::~Array(){
    delete[] m_p;
}
int& Array::operator[](int i){
    return m_p[i];
}
const int & Array::operator[](int i) const{
    return m_p[i];
}
void Array::display() const{
    for(int i = 0; i < m_length; i++){
        if(i == m_length - 1){
            cout<<m_p[i]<<endl;
        }else{
            cout<<m_p[i]<<", ";
        }
    }
}
int main(){
    int n;
    cin>>n;
    Array A(n);
    for(int i = 0, len = A.length(); i < len; i++){
        A[i] = i * 5;
    }
    A.display();
   
    const Array B(n);
    cout<<B[n-1]<<endl;  //访问最后一个元素
   
    return 0;
}

运行结果:
5↙
0, 5, 10, 15, 20
33685536

重载[ ]运算符以后,表达式arr[i]会被转换为:

arr.operator[ ](i);

B 是 const 对象,如果 Array 类没有提供 const 版本的operator[ ],那么第 60 行代码将报错。

ps:其实以上代码虽然看着比较杂糅,但其实关键语句只有这两句

int& Array::operator[](int i) {
    return m_p[i];
}
const int& Array::operator[](int i) const {
    return m_p[i];

通过这两句对[]进行重载,使得array类实现了变长数组的功能。


重载++和–(自增和自减运算符)详解

自增++和自减--都是一元运算符,它的前置形式和后置形式都可以被重载。

#include <iostream>
#include <iomanip>
using namespace std;

//秒表类
class stopwatch{
public:
    stopwatch(): m_min(0), m_sec(0){ }
public:
    void setzero(){ m_min = 0; m_sec = 0; }
    stopwatch run();  // 运行
    stopwatch operator++();  //++i,前置形式
    stopwatch operator++(int);  //i++,后置形式
    friend ostream & operator<<( ostream &out, const stopwatch &);
private:
    int m_min;  //分钟
    int m_sec;  //秒钟
};

stopwatch stopwatch::run(){
    ++m_sec;
    if(m_sec == 60){
        m_min++;
        m_sec = 0;
    }
    return *this;
}

stopwatch stopwatch::operator++(){
    return run();
}

stopwatch stopwatch::operator++(int n){
    stopwatch s = *this;
    run();
    return s;
}

ostream &operator<<( ostream & out, const stopwatch & s){
    out<<setfill('0')<<setw(2)<<s.m_min<<":"<<setw(2)<<s.m_sec;
    return out;
}

int main(){
    stopwatch s1, s2;

    s1 = s2++;
    cout << "s1: "<< s1 <<endl;
    cout << "s2: "<< s2 <<endl;
    s1.setzero();
    s2.setzero();

    s1 = ++s2;
    cout << "s1: "<< s1 <<endl;
    cout << "s2: "<< s2 <<endl;
    return 0;
}

operator++() 函数实现自增的前置形式,直接返回 run() 函数运行结果即可。

operator++ (int n) 函数实现自增的后置形式,返回值是对象本身,但是之后再次使用该对象时,对象自增了,所以在该函数的函数体中,先将对象保存,然后调用一次 run() 函数,之后再将先前保存的对象返回。在这个函数中参数n是没有任何意义的,它的存在只是为了区分是前置形式还是后置形式。(妙啊)

自减运算符的重载与上面类似,这里不再赘述。


重载new和delete运算符详解

内存管理运算符 new、new[]、delete 和 delete[] 也可以进行重载,其重载形式既可以是类的成员函数,也可以是全局函数。

以成员函数的形式重载 new 运算符:

void * className::operator new( size_t size ){
  //TODO:
}

以全局函数的形式重载 new 运算符:

void * operator new( size_t size ){
  //TODO:
}

两种重载形式的返回值相同,都是void *类型,并且都有一个参数,为size_t类型。

在重载 new 或 new[] 时,无论是作为成员函数还是作为全局函数,它的第一个参数必须是 size_t 类型。size_t 表示的是要分配空间的大小,对于 new[] 的重载函数而言,size_t 则表示所需要分配的所有空间的总和。重载函数也可以有其他参数,但都必须有默认值,并且第一个参数的类型必须是 size_t。

size_t 在头文件 中被定义为typedef unsigned int size_t;,也就是无符号整型。


同样的,delete 运算符也有两种重载形式。以类的成员函数的形式进行重载:

void className::operator delete( void *ptr){
  //TODO:
}

以全局函数的形式进行重载:

void operator delete( void *ptr){
  //TODO:
}

两种重载形式的返回值都是 void 类型,并且都必须有一个 void 类型的指针作为参数,该指针指向需要释放的内存空间。


重载()(强制类型转换运算符)

c++中类型的名字(包括类的名字)本身也是一种运算符,即类型强制转换运算符。

类型强制转换运算符是单目运算符,也可以被重载,但只能重载为成员函数,不能重载为全局函数。经过适当重载后,(类型名)对象这个对对象进行强制类型转换的表达式就等价于对象.operator 类型名(),即变成对运算符函数的调用。

下面的程序对 double 类型强制转换运算符进行了重载。

#include <iostream>
using namespace std;
class Complex
{
    double real, imag;
public:
    Complex(double r = 0, double i = 0) :real(r), imag(i) {};
    operator double() { return real; }//重载强制类型转换运算符double
};
int main()
{
    Complex c(1.2, 3.4);
    cout << (double)c << endl;  //输出 1.2
    double n = 2 + c;  //等价于 double n = 2 + c. operator double()
    cout << n;  //输出 3.2
}

本例相当于是把对象c转换为了double类型的1.2

第 8 行对 double 运算符进行了重载。重载强制类型转换运算符时,不需要指定返回值类型,因为返回值类型是确定的,就是运算符本身代表的类型,在这里就是 double。

重载后的效果是,第 13 行的(double)c等价于c.operator double()

第 14 行,编译器认为本行中c这个位置如果出现的是 double 类型的数据,就能够解释得通,而 Complex 类正好重载了 double 运算符,因而本行就等价于:

double n = 2 + c.operator double();

运算符重载注意事项以及汇总

在c++中进行运算符重载时,有以下问题需要注意:

  • 重载后运算符的含义应该符合原有用法习惯。例如重载+运算符,完成的功能就应该类似于做加法,在重载的+运算符中做减法是不合适的。此外,重载应尽量保留运算符原有的特性。
  • C++ 规定,运算符重载不改变运算符的优先级。
  • 以下运算符不能被重载:..*::? :sizeof
  • 重载运算符()[]->、或者赋值运算符=时,只能将它们重载为成员函数,不能重载为全局函数。
  • 运算符重载的实质是将运算符重载为一个函数,使用运算符的表达式就被解释为对重载函数的调用。
  • 运算符可以重载为全局函数。此时函数的参数个数就是运算符的操作数个数,运算符的操作数就成为函数的实参。
  • 运算符也可以重载为成员函数。此时函数的参数个数就是运算符的操作数个数减一,运算符的操作数有一个成为函数作用的对象,其余的成为函数的实参。
  • 必要时需要重载赋值运算符=,以避免两个对象内部的指针指向同一片存储空间。
  • 运算符可以重载为全局函数,然后声明为类的友元。
  • <<和>>是在 iostream 中被重载,才成为所谓的“流插入运算符”和“流提取运算符”的。
  • 类型的名字可以作为强制类型转换运算符,也可以被重载为类的成员函数。它能使得对象被自动转换为某种类型。
  • 自增、自减运算符各有两种重载方式,用于区别前置用法和后置用法。
  • 运算符重载不改变运算符的优先级。重载运算符时,应该尽量保留运算符原本的特性。

基础的笔记就更新到这里了,会再记录一篇高级部分的笔记来记录比较难的内容


2021.4.27 21:15